<?php

session_start(); // 开启session功能
/*
 * LogAdminLite 1.1
 * 开源作品-PHP在线通用日志浏览器(单文件绿色版)-LogAdminLite 1.0
 * 程序特性：
 *      1. 单文件绿色版,部署超简单
 *      2. 支持目录树形层级结构显示，支持目录展开或收起
 *      3. 支持日志文件下载、文件删除、文件查看、目录清空功能
 *      4. 支持用户登录验证、IP验证等多种权限验证功能
 * 使用方法：
 *      1. 复制本文件到web任意目录
 *      2. 修改$authconfig配置信息为实际内容
 *      3. 在浏览器输入链接路径,开始使用
 * 说明：写这个程序主要是为了辅助项目开发中的日志文件查看,提供了程序特性描述的功能(基本能满足项目开发需要). 如果您有兴趣一起完善这个小程序,欢迎使用git一起开发~感谢！
 * 项目周期：
 *      2017-8-16 立项开发
 *      2017-8-16 程序完成
 * 版权：不限商业用途! 如有修改代码请保留原著署名,并把修改后的代码回馈到作者邮箱：<14507247@qq.com>,促进本产品进步.做一个有公德心的正能量程序员~
 *      作者博客：http://www.cnblogs.com/sochishun/ 欢迎交流~
 * @since 1.0 <2017-8-16> SoChishun <14507247@qq.com> Added.
 * @since 1.1 <2017-8-28> SoChishun 新增子目录内容视图功能，跳转限定在子目录中；新增清空目录功能
 */

/**
 * 页面访问验证配置
 */
$authconfig = [
    'admins' => ['admin' => 'admin123', 'testuser' => 'testpassword'], // 允许登录的帐号列表['登录名'=>'密码']
    'ips' => ['127.0.0.1', '120.42.7.179'], // 允许访问的ip地址列表
    'current_authtype' => 3, // 指定页面访问验证类型：0:无需验证;1:帐号密码验证;2:IP验证;3:混合验证(先IP验证,IP验证失败则转入帐号密码验证)
];
// 日志根目录
define('ROOTPATH', './java/logs/');

// 版本号
define('VERSION', '1.1.0');
// 程序名称
define('APPNAME', 'LogAdminLite');
// 缓存前缀
define('SESSION_PREFIX', 'logadmlite');

// 访问权限验证
$current_authtype = $authconfig['current_authtype'];
$authstatus = true; // 权限验证状态
if ($current_authtype == 2 || $current_authtype == 3) { // IP验证
    $ip = $_SERVER['REMOTE_ADDR'];
    if (!in_array($ip, $authconfig['ips'])) {
        $authstatus = false; // IP验证失败
        if ($current_authtype == 2) {
            exit('IP [' . $ip . '] 禁止访问!');
        }
    }
}
if ($current_authtype == 1 || ($current_authtype == 3 && !$authstatus)) { // 帐号密码验证
    $loginid = isset($_SESSION[SESSION_PREFIX . 'user']) ? $_SESSION[SESSION_PREFIX . 'user'] : false;
    $token = get_token();
    if (!$token) {
        $token = generate_token();
    }
// 注销登录
    if (input('action') == 'logout') {
        logout();
        exit;
    }
    if (!$loginid) {
        checklogin($token, $authconfig['admins']);
        exit;
    }
}

$path = input('path'); // 路径
$dirpath = input('dirpath'); // 目录路径
$action = input('action'); // 操作
//debugmsg('logadminlite.php[0]', 'debugtest');

if ($path) {
    if (!file_exists($path)) {
        exit('路径 [' . $path . '] 不存在! <a href="?path' . ROOTPATH . '">[返回]</a>');
    }
    if ($action == "delete" || $action == "clear") {
        try {
            if (is_dir($path)) {
                fn_rmdir($path, 'delete' == $action);
            } else {
                unlink($path);
            }
        } catch (Exception $ex) {
            echo $ex->getMessage();
        }
        echo 'delete' == $action ? '删除成功!' : '清空成功!';
        if ($dirpath) {
            header('location:?path=' . $dirpath . '&dirpath=' . $dirpath);
        } else {
            header('location:?path=' . ROOTPATH);
        }
        exit;
    }
}

echo '<!DOCTYPE html><html><head><meta charset="UTF-8"><title>', ($path ? short_text(basename($path), 16, true) . ' - ' : ''), APPNAME, ' - ', VERSION, '</title><style type="text/css">body{font-size:13px;}a{text-decoration:none}.aside{width:240px;max-height:600px;overflow:auto;float:left;font-size:13px}.main{width:800px;float:left;margin-left:10px;}ul{margin:0}li{list-style:none;margin-left:-40px;}li.active { font-weight: bold; color:#F00;}li.active a{ color:#F00;}</style></head><body><script src="//code.jquery.com/jquery-1.11.3.min.js"></script>';
echo '<h1>', APPNAME, ' <a href="?new-window" title="新开一个页面" target="_blank" style="font-size:12px;">[N]</a></h1>';
// 用户登录信息
if (isset($loginid)) {
    echo $loginid;
    echo ' [<a href="?action=logout" onclick="return confirm(\'您确定要注销登录吗?\');">注销登录</a>]';
}
echo '<div style="color:#888;margin-bottom:3px;">按钮名称说明：[n]:在新窗口中打开地址; [x]:删除文件或目录; [c]:清空目录; [&darr;]:下载文件(右键另存为); [r]:刷新</div>';
echo '<div><a href="?path=', ROOTPATH, '">[根目录]</a></div>';
if ($path) {
    if ($dirpath) { // 限定在子目录中
        echo '<div style="margin:3px 0;">当前路径：', $dirpath;
        echo ' [<a title="refresh,刷新" href="?path=' . $dirpath . '&dirpath=', $dirpath, '">r</a>]';
        echo '[<a title="clear,清空目录" href="?path=' . $dirpath . '&dirpath=' . $dirpath . '&action=clear" onclick="return confirm(\'您确定要清空当前目录吗?\')">c</a>]';
        echo '[<a title="delete,删除目录" href="?path=' . $dirpath . '&dirpath=' . $dirpath . '&action=delete" onclick="return confirm(\'您确定要删除当前目录吗?\')">x</a>]';
        echo '</div>';
    } else { // 限定在根目录中
        echo '<div style="margin:3px 0;">当前路径：', $path;
        echo ' [<a title="refresh,刷新" href="?path=' . $path . '">r</a>]';
        if (is_file($path)) {
            echo '[<a title="download [右键点击下载]" href="' . $path . '">&darr;</a>]';
        }
        if (is_dir($path)) {
            echo '[<a title="clear,清空目录" href="?path=' . $path . '&dirpath=' . $path . '&action=clear" onclick="return confirm(\'您确定要清空当前目录吗?\')">c</a>]';
            echo '[<a title="delete,删除目录" href="?path=' . $path . '&dirpath=' . $path . '&action=delete" onclick="return confirm(\'您确定要删除当前目录吗?\')">x</a>]';
        }
        echo '</div>';
    }
}
echo '<div class="aside">';
echo '<ul>';
if ($dirpath) {
    fn_scandir($dirpath, 0, true, $path, $dirpath);
} else {
    fn_scandir(($path && is_dir($path) ? $path : ROOTPATH), 0, true, $path, $dirpath);
}
echo '</ul>';
echo '</div>';

if ($path && is_file($path)) {
    echo '<div class="main">';
    echo 'size:', format_bytes(filesize($path)), ', modified time:', date('Y-m-d H:i:s', filectime($path)), '<br />';
    echo '<textarea cols="150" rows="40">', htmlspecialchars(fn_tail($path)), '</textarea>';
    echo '</div>';
}
echo '<div style="clear:both"></div>';
echo '<script>$(".btn_switch").click(function () {var $this = $(this);if ($this.text() == "[-]") {$this.text("[+]").attr("title", "展开");$this.parent().children("ul").hide();} else {$this.text("[-]").attr("title", "收起");$this.parent().children("ul").show();}});</script>';
echo '</body></html>';

/**
 * 调试
 * @param type $location
 * @param type $msg
 */
function debugmsg($location, $msg) {
    $content = date('Y-m-d H:i:s') . PHP_EOL . $location . PHP_EOL . (is_string($msg) ? $msg : var_export($msg, true)) . PHP_EOL . PHP_EOL;
    $dir = './java/logs/';
    if (!is_dir($dir)) {
        mkdir($dir);
    }
    $path = $dir . 'log-' . date('mdh') . '.txt';
    fn_write($path, $content);
    //file_put_contents($path, $content, FILE_APPEND);
}

/**
 * PHP高效遍历文件夹（大量文件不会卡死）
 * <br />外层要套上UL标签
 * @param string $path 目录路径
 * @param integer $level 目录深度
 * @param boolean $showfile 是否显示文件(否则只遍历显示目录)
 * @param string $curpath 当前输入的目录
 */
function fn_scandir($path = './', $level = 0, $showfile = true, $curpath = '', $dirpath = '') {
    $file = new FilesystemIterator($path);
    $filename = '';
    $url = '';
    $prefix = ''; // 树形层级图形
    $isactive = ''; // 选中效果类
    foreach ($file as $fileinfo) {
        $filename = $fileinfo->getFilename();
        $filepath = $path . $filename;
        $isactive = ($curpath == $filepath ? ' class="active"' : '');
        $prefix = $level > 0 ? ('|' . str_repeat('--', $level)) : '';
        if ($fileinfo->isDir()) {
            $filepath = $filepath . '/';
            $url = '<a title="查看目录内容 ' . $filepath . '" href="?path=' . $filepath . '&dirpath=' . $filepath . '">' . short_text($filename, 16) . '</a>';
            $url .= '[<a title="new window,新窗口打开目录 ' . $filepath . '" href="?path=' . $filepath . '&dirpath=' . $dirpath . '" target="_blank">n</a>]';
            $url .= '[<a title="refresh,刷新目录" href="?path=' . ($dirpath ? $dirpath : ROOTPATH) . '&dirpath=' . $dirpath . '">r</a>]';
            $url .= '[<a title="clear,清空目录" href="?path=' . $filepath . '&dirpath=' . $dirpath . '&action=clear" onclick="return confirm(\'您确定要清空当前目录吗?\')">c</a>]';
            $url .= '[<a title="delete,删除目录" href="?path=' . $filepath . '&dirpath=' . $dirpath . '&action=delete" onclick="return confirm(\'您确定要删除当前目录吗?\')">x</a>]';
            echo '<li', $isactive, '><strong>' . $prefix . $url . '/</strong> <a href="#" class="btn_switch" title="收起">[-]</a>' . PHP_EOL;
            echo '<ul>';
            fn_scandir($filepath, $level + 1, $showfile, $curpath);
            echo '</ul>';
            echo '</li>';
        } else {
            if ($showfile) {
                $url = '<a title="查看文件内容 ' . $filepath . '" href="?path=' . $filepath . '&dirpath=' . $dirpath . '">' . short_text($filename, 16) . '</a> [<a title="new window,新窗口打开文件 ' . $filepath . '" href="?path=' . $filepath . '&dirpath=' . $dirpath . '" target="_blank">n</a>][<a title="delete" href="?path=' . $filepath . '&dirpath=' . $dirpath . '&action=delete" onclick="return confirm(\'您确定要删除吗?\')">x</a>][<a title="download [右键点击下载]" href="' . $filepath . '" target="_blank">&darr;</a>]';
                echo '<li', $isactive, '>' . $prefix . $url . '</li>' . PHP_EOL;
            }
        }
        /*
          if ($fileinfo->isDir()) {
          fn_scandir($filepath, $level + 1);
          } */
    }
}

/**
 * 删除非空目录里面所有文件和子目录
 * @param string $dir
 * @param boolean $delSelf 是否删除自身目录(删除还是清空)
 * @return boolean
 */
function fn_rmdir($dir, $delSelf = true) {
    //先删除目录下的文件：
    $dh = opendir($dir);
    while ($file = readdir($dh)) {
        if ($file != "." && $file != "..") {
            $fullpath = $dir . "/" . $file;
            if (is_dir($fullpath)) {
                fn_rmdir($fullpath, true);
            } else {
                unlink($fullpath);
            }
        }
    }
    closedir($dh);
    //删除当前文件夹：
    if (!$delSelf || rmdir($dir)) {
        return true;
    } else {
        return false;
    }
}

/**
 * PHP高效读取文件
 * @param string $filepath
 * @return string
 */
function fn_tail($filepath) {
    if (file_exists($filepath)) {
        $fp = fopen($filepath, "r");
        $str = "";
        $buffer = 1024; //每次读取 1024 字节
        while (!feof($fp)) {//循环读取，直至读取完整个文件
            $str .= fread($fp, $buffer);
        }
        return $str;
    }
}

/**
 * PHP高效写入文件（支持并发）
 * @param string $filepath
 * @param string $content
 */
function fn_write($filepath, $content) {
    if ($fp = fopen($filepath, 'a')) {
        $startTime = microtime();
        // 对文件进行加锁时，设置一个超时时间为1ms，如果这里时间内没有获得锁，就反复获得，直接获得到对文件操作权为止，当然。如果超时限制已到，就必需马上退出，让出锁让其它进程来进行操作。
        do {
            $canWrite = flock($fp, LOCK_EX);
            if (!$canWrite) {
                usleep(round(rand(0, 100) * 1000));
            }
        } while ((!$canWrite) && ((microtime() - $startTime) < 1000));
        if ($canWrite) {
            fwrite($fp, $content);
        }
        fclose($fp);
    }
}

/**
 * 获取用户输入
 * @param string $name
 * @param mixed $defv
 * @return mixed
 */
function input($name, $defv = '') {
    return isset($_REQUEST[$name]) ? $_REQUEST[$name] : $defv;
}

/**
 * 截短字符串
 * @param string $str
 * @param integer $length
 * @param boolean $behind
 * @return string
 */
function short_text($str, $length, $behind = false) {
    $len = strlen($str);
    if ($len <= $length) {
        return $str;
    }
    return $behind ? '...' . substr($str, $len - $length) : substr($str, 0, $len) . '...';
}

/**
 * 获取表单令牌
 * @return string
 */
function get_token() {
    return isset($_SESSION[SESSION_PREFIX . 'token']) ? $_SESSION[SESSION_PREFIX . 'token'] : false;
}

/**
 * 生成表单令牌
 * @return string
 */
function generate_token() {
    $token = date('ymdhis') . rand(100, 999);
    $_SESSION[SESSION_PREFIX . 'token'] = $token;
    return $token;
}

/**
 * 检测用户登录
 */
function checklogin($token, $admins) {
    $posttoken = htmlspecialchars(input('token'));
    if ($posttoken) {
        generate_token(); // 重新生成token
        if ($posttoken != $token) {
            exit('非法来源!');
        }
        $loginid = strval(htmlspecialchars(input('user')));
        $loginpwd = htmlspecialchars(input('pass'));
        if (!$loginid || !$loginpwd) {
            exit('表单填写不完整!');
        }
        if (!array_key_exists($loginid, $admins)) {
            exit('帐号不存在!');
        }
        if ($loginpwd != $admins[$loginid]) {
            exit('密码错误!');
        }
        $_SESSION[SESSION_PREFIX . 'user'] = $loginid;
        header('location:?loginsuccess');
    } else {
        echo '<form method="post" action="?action=login">';
        echo '<input type="text" name="user" placeholder="用户名" required="required" />';
        echo '<input type="password" name="pass" placeholder="密码" required="required" />';
        echo '<input type="hidden" name="token" value="', $token, '" />';
        echo '<button type="submit">登录</button><button type="reset">重置</button> ';
        echo $_SERVER['REMOTE_ADDR'];
        echo '</form>';
    }
}

/**
 * 注销登录
 */
function logout() {
    unset($_SESSION[SESSION_PREFIX . 'user']);
    unset($_SESSION[SESSION_PREFIX . 'token']);
    header('location:?logoutsuccess');
}

/**
 * 格式化文件字节大小
 * @param int $size
 * @return string
 */
function format_bytes($size) {
    $units = array(' B', ' KB', ' MB', ' GB', ' TB');
    for ($i = 0; $size >= 1024 && $i < 4; $i++)
        $size /= 1024;
    return round($size, 2) . $units[$i];
}
